mod migration;
mod post;

use hypers::hyper::StatusCode;
use hypers::prelude::*;
use hypers::tracing::info;
use migration::{Migrator, MigratorTrait};
use post::{ActiveModel, Model};
use sea_orm::{entity::*, query::*, DatabaseConnection};
use tera::Tera;

const DEFAULT_POSTS_PER_PAGE: u64 = 5;
const APP_STATE: &str = "APP_STATE";
#[derive(Debug)]
struct AppState {
    templates: tera::Tera,
    conn: DatabaseConnection,
}
struct Api;
#[controller]
impl Api {
    #[post("/")]
    async fn create(req: &mut Request) -> Result<Response> {
        let form = req.parse_form::<Model>().await?;
        let state = req.depot.get::<AppState>(APP_STATE).unwrap();
        ActiveModel {
            title: Set(form.title.to_owned()),
            text: Set(form.text.to_owned()),
            ..Default::default()
        }
        .save(&state.conn)
        .await?;
        let mut res = Response::default();
        res.redirect(StatusCode::FOUND, "/");
        Ok(res)
    }

    #[get("/")]
    async fn list(req: &mut Request) -> Result<Response> {
        let state = req.depot.get::<AppState>(APP_STATE).unwrap();
        let page = req.query("page").unwrap_or(1);
        let posts_per_page = req
            .query("posts_per_page")
            .unwrap_or(DEFAULT_POSTS_PER_PAGE);
        let paginator = post::Entity::find()
            .order_by_asc(post::Column::Id)
            .paginate(&state.conn, posts_per_page);
        let num_pages = paginator.num_pages().await?;
        let posts = paginator.fetch_page(page - 1).await?;
        let mut ctx = tera::Context::new();
        ctx.insert("posts", &posts);
        ctx.insert("page", &page);
        ctx.insert("posts_per_page", &posts_per_page);
        ctx.insert("num_pages", &num_pages);
        let body = state.templates.render("index.html", &ctx)?;
        let res = Response::default();
        Ok(res.render(Text::Html(body)))
    }

    #[get("/new")]
    async fn new(req: &mut Request) -> Result<Response> {
        let state = req.depot.get::<AppState>(APP_STATE).unwrap();
        let ctx = tera::Context::new();
        let body = state.templates.render("new.html", &ctx)?;
        let res = Response::default();
        Ok(res.render(Text::Html(body)))
    }

    #[get("/:id")]
    async fn edit(req: &mut Request) -> Result<Response> {
        let state = req.depot.get::<AppState>(APP_STATE).unwrap();
        let id = req.param::<i32>("id").unwrap_or_default();
        let post: post::Model = post::Entity::find_by_id(id)
            .one(&state.conn)
            .await
            .map_err(|_| StatusError::internal_server_error())?
            .ok_or_else(StatusError::not_found)?;
        let mut ctx = tera::Context::new();
        ctx.insert("post", &post);
        let body = state.templates.render("edit.html", &ctx)?;
        let res = Response::default();
        Ok(res.render(Text::Html(body)))
    }

    #[post("/:id")]
    async fn update(req: &mut Request) -> Result<Response> {
        let form = req
            .parse_form::<post::Model>()
            .await
            .map_err(|_| StatusError::bad_request())?;
        let id = req.param::<i32>("id").unwrap_or_default();
        let state = req.depot.get::<AppState>(APP_STATE).unwrap();
        post::ActiveModel {
            id: Set(id),
            title: Set(form.title.to_owned()),
            text: Set(form.text.to_owned()),
        }
        .save(&state.conn)
        .await
        .map_err(|_| StatusError::internal_server_error())?;
        let mut res = Response::default();
        res.redirect(StatusCode::FOUND, "/");
        Ok(res)
    }

    #[post("/delete/:id")]
    async fn delete(req: &mut Request) -> Result<Response> {
        let state = req.depot.get::<AppState>(APP_STATE).unwrap();
        let id = req.param::<i32>("id").unwrap_or_default();
        let post: post::ActiveModel = post::Entity::find_by_id(id)
            .one(&state.conn)
            .await
            .map_err(|_| StatusError::internal_server_error())?
            .ok_or_else(StatusError::not_found)?
            .into();
        post.delete(&state.conn)
            .await
            .map_err(|_| StatusError::internal_server_error())?;
        let mut res = Response::default();
        res.redirect(StatusCode::FOUND, "/");
        Ok(res)
    }
}
#[hook]
async fn store_app_state(mut req: Request, next: Next<'_>) -> Result<Response> {
    let db_url = "sqlite://db_sea_orm/db.sqlite";
    // create post table if not exists
    let conn = sea_orm::Database::connect(db_url).await?;
    Migrator::up(&conn, None).await.unwrap();
    let templates = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*"))?;
    req.depot.insert(APP_STATE, AppState { templates, conn });
    Ok(next.next(req).await)
}

#[tokio::main]
async fn main() -> Result<()> {
    std::env::set_var("RUST_LOG", "debug");
    tracing_subscriber::fmt::init();
    let server_url = "0.0.0.0:7878";
    info!("Starting server at {server_url}");
    let mut root: Router = Api.into();
    root.get(
        "/static/*",
        StaticDir::new(concat!(env!("CARGO_MANIFEST_DIR"), "/static")),
    ); // http://0.0.0.0:7878/static/
    root.hook(store_app_state, None, None);
    info!("root = {:#?}", root);
    let listener = hypers::TcpListener::bind(server_url).await?;
    hypers::listen(root, listener).await
}
